package cn.jetbrain.netbeans.pdfviewer;

import cn.jetbrain.netbeans.pdfviewer.view.Flag;
import cn.jetbrain.netbeans.pdfviewer.view.FullScreenWindow;
import cn.jetbrain.netbeans.pdfviewer.view.PageChangeListener;
import cn.jetbrain.netbeans.pdfviewer.view.PagePanel;
import cn.jetbrain.netbeans.pdfviewer.view.ThumbPanel;
import com.sun.pdfview.OutlineNode;
import com.sun.pdfview.PDFDestination;
import com.sun.pdfview.PDFFile;
import com.sun.pdfview.PDFObject;
import com.sun.pdfview.PDFPage;
import com.sun.pdfview.action.GoToAction;
import com.sun.pdfview.action.PDFAction;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.ButtonGroup;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.InputMap;
import javax.swing.JButton;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.JToggleButton;
import javax.swing.JToolBar;
import javax.swing.JTree;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.tree.DefaultTreeCellRenderer;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileUtil;
import org.openide.util.Exceptions;
import org.openide.util.Utilities;
import org.openide.windows.CloneableTopComponent;
import org.openide.windows.Mode;
import org.openide.windows.TopComponent;
import org.openide.windows.WindowManager;

public class PDFViewer extends CloneableTopComponent implements KeyListener, TreeSelectionListener, PageChangeListener {

    public final static Color DEFAULT_BACKGROUND = Color.DARK_GRAY;
    public final static Color THUMBS_BACKGROUND = new Color(0xF5F5F5);

    PDFFile curFile;
    int curpage = -1;

    ThumbPanel thumbs;
    public JScrollPane thumbsPane;

    JTree outlineTree;
    OutlineNode outline;
    public JScrollPane outlinePane;
    
    JScrollPane pageScroll;
    PagePanel page;
    PagePanel fspp;
    
    JToggleButton fullScreenButton;
    public JToggleButton thumbButton;
    public JToggleButton outlineButton;
    JTextField pageField;
    FullScreenWindow fullScreen;
    Flag docWaiter;
    PagePreparer pagePrep;

    private JToggleButton fitPageButton;
    private JToggleButton fitHeightButton;
    private JToggleButton fitWidthButton;
    private JToggleButton actualSizeButton;
    private ButtonGroup fitButtonGroup;

    private Action unitScrollUp;
    private Action unitScrollDown;
    private Action scrollUp;
    private Action scrollDown;

    TopComponent old;

    /**
     * Utility
     *
     * @deprecated in 6.5 ImageUtilities
     *
     * @param name
     * @return
     */
    public Icon getIcon(String name) {
        Icon icon = null;
        try {
            icon = new ImageIcon(Utilities.loadImage(name));
        } catch (Exception e) {
            Exceptions.printStackTrace(e);
        }
        return icon;
    }

    Action fitPageAction = new AbstractAction("Fit Page", getIcon("cn/jetbrain/netbeans/pdfviewer/view/res/fitPage.png")) {
        public void actionPerformed(ActionEvent evt) {
            if (fitPageButton.isSelected()) {
                doZoom(-1.0);
            }
        }
    };
    Action fitHeightAction = new AbstractAction("Fit Height", getIcon("cn/jetbrain/netbeans/pdfviewer/view/res/hFit.png")) {

        public void actionPerformed(ActionEvent evt) {
            if (fitHeightButton.isSelected()) {
                doZoom(-2.0);
            }
        }
    };
    Action fitWidthAction = new AbstractAction("Fit Width", getIcon("cn/jetbrain/netbeans/pdfviewer/view/res/wFit.png")) {

        public void actionPerformed(ActionEvent evt) {
            if (fitWidthButton.isSelected()) {
                doZoom(-3.0);
            }
        }
    };
    Action actualSizeAction = new AbstractAction("Actual Size", getIcon("cn/jetbrain/netbeans/pdfviewer/view/res/actualSize.png")) {

        public void actionPerformed(ActionEvent evt) {
            if (actualSizeButton.isSelected()) {
                doZoom(-4.0);
            }
        }
    };
    Action zoomInAction = new AbstractAction("zoomIn", getIcon("cn/jetbrain/netbeans/pdfviewer/view/res/zoomIn.png")) {

        public void actionPerformed(ActionEvent evt) {
            fitButtonGroup.clearSelection();
            doZoom(page.getScale() * 1.1);
        }
    };
    Action zoomOutAction = new AbstractAction("zoomOut", getIcon("cn/jetbrain/netbeans/pdfviewer/view/res/zoomOut.png")) {

        public void actionPerformed(ActionEvent evt) {
            fitButtonGroup.clearSelection();
            doZoom(page.getScale() * 0.9);
        }
    };
    Action thumbAction = new AbstractAction("Thumb", getIcon("cn/jetbrain/netbeans/pdfviewer/view/res/thumb.png")) {

        public void actionPerformed(ActionEvent evt) {
            doThumb();
        }
    };

    public void doThumb() {
        if (thumbsPane == null) {
            thumbs = new ThumbPanel(curFile);
            thumbs.addPageChangeListener(this);
            thumbsPane = new JScrollPane(thumbs, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
            thumbs.pageShown(curpage);
        }
        doView(ThumbView.findInstance(), thumbButton.isSelected());
    }
    Action outlineAction = new AbstractAction("Outline", getIcon("cn/jetbrain/netbeans/pdfviewer/view/res/outline.png")) {
        public void actionPerformed(ActionEvent evt) {
            doOutline();
        }
    };

    public void doOutline() {
        if (outlinePane == null) {
            outlineTree = new JTree(outline);
            outlineTree.setCellRenderer(new DefaultTreeCellRenderer() {

                @Override
                public Icon getIcon() {
                    return PDFViewer.this.getIcon("cn/jetbrain/netbeans/pdfviewer/view/res/node.png");
                }
            });
            outlineTree.setRowHeight(20);
            outlineTree.setRootVisible(false);
            outlineTree.addTreeSelectionListener(PDFViewer.this);
            outlinePane = new JScrollPane(outlineTree);
        }
        doView(OutlineView.findInstance(), outlineButton.isSelected());
    }

    private void doView(View v, boolean show) {
        if (show) {
            v.setPDFViewer(this);
            v.open();
            v.requestActive();
            requestActive();
        } else {
            if (v.isOpened() && v.getPDFViewer().getName().equals(getName())) {
                v.forceClose();
            }
        }
    }
    Action fullScreenAction = new AbstractAction("Full screen", getIcon("cn/jetbrain/netbeans/pdfviewer/view/res/fullscr.png")) {
        public void actionPerformed(ActionEvent evt) {
            doFullScreen((evt.getModifiers() & ActionEvent.SHIFT_MASK) != 0);
        }
    };
    Action nextAction = new AbstractAction("Next", getIcon("cn/jetbrain/netbeans/pdfviewer/view/res/next.png")) {

        public void actionPerformed(ActionEvent evt) {
            doNext();
        }
    };
    Action firstAction = new AbstractAction("First", getIcon("cn/jetbrain/netbeans/pdfviewer/view/res/first.png")) {

        public void actionPerformed(ActionEvent evt) {
            doFirst();
        }
    };
    Action lastAction = new AbstractAction("Last", getIcon("cn/jetbrain/netbeans/pdfviewer/view/res/last.png")) {

        public void actionPerformed(ActionEvent evt) {
            doLast();
        }
    };
    Action prevAction = new AbstractAction("Prev", getIcon("cn/jetbrain/netbeans/pdfviewer/view/res/prev.png")) {

        public void actionPerformed(ActionEvent evt) {
            doPrev();
        }
    };

    PDFViewer(PDFDataObject o) {
        setLayout(new BorderLayout());
        page = new PagePanel();
        page.addKeyListener(this);
        pageScroll = new JScrollPane(page);

        //custom page panel key function
        InputMap im = pageScroll.getInputMap(WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
        unitScrollUp = pageScroll.getActionMap().get(im.get(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0)));
        unitScrollDown = pageScroll.getActionMap().get(im.get(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0)));
        scrollUp = pageScroll.getActionMap().get(im.get(KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_UP, 0)));
        scrollDown = pageScroll.getActionMap().get(im.get(KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_DOWN, 0)));
        im.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0), "none");
        im.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0), "none");
        im.put(KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_UP, 0), "none");
        im.put(KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_DOWN, 0), "none");

        add(pageScroll, BorderLayout.CENTER);
        pageScroll.addComponentListener(new ComponentAdapter() {
            @Override
            public void componentResized(ComponentEvent e) {
                super.componentResized(e);
                PDFViewer.this.gotoPage(curpage);
            }
        });

        JToolBar toolbar = new JToolBar();
        toolbar.setFloatable(false);

        JButton jb;
        jb = new JButton(firstAction);
        jb.setText("");
        toolbar.add(jb);
        jb = new JButton(prevAction);
        jb.setText("");
        toolbar.add(jb);
        pageField = new JTextField("-", 3);
        pageField.setMaximumSize(new Dimension(45, 32));
        pageField.addActionListener(new ActionListener() {

            public void actionPerformed(ActionEvent evt) {
                doPageTyped();
            }
        });
        toolbar.add(pageField);
        jb = new JButton(nextAction);
        jb.setText("");
        toolbar.add(jb);
        jb = new JButton(lastAction);
        jb.setText("");
        toolbar.add(jb);

        toolbar.addSeparator();
        thumbButton = new JToggleButton(thumbAction);
        thumbButton.setText("");
        toolbar.add(thumbButton);
        thumbButton.setEnabled(true);

        outlineButton = new JToggleButton(outlineAction);
        outlineButton.setText("");
        toolbar.add(outlineButton);
        outlineButton.setEnabled(true);

        fullScreenButton = new JToggleButton(fullScreenAction);
        fullScreenButton.setText("");
        toolbar.add(fullScreenButton);
        fullScreenButton.setEnabled(true);

        toolbar.addSeparator();

        toolbar.addSeparator();

        fitButtonGroup = new ButtonGroup();

        fitPageButton = new JToggleButton(fitPageAction);
        fitPageButton.setText("");
        fitButtonGroup.add(fitPageButton);
        fitPageButton.setSelected(true);
        toolbar.add(fitPageButton);
        fitHeightButton = new JToggleButton(fitHeightAction);
        fitHeightButton.setText("");
        fitButtonGroup.add(fitHeightButton);
        toolbar.add(fitHeightButton);
        fitWidthButton = new JToggleButton(fitWidthAction);
        fitWidthButton.setText("");
        fitButtonGroup.add(fitWidthButton);
        toolbar.add(fitWidthButton);
        actualSizeButton = new JToggleButton(actualSizeAction);
        actualSizeButton.setText("");
        fitButtonGroup.add(actualSizeButton);
        toolbar.add(actualSizeButton);

        jb = new JButton(zoomInAction);
        jb.setText("");
        toolbar.add(jb);
        jb = new JButton(zoomOutAction);
        jb.setText("");
        toolbar.add(jb);

        add(toolbar, BorderLayout.NORTH);
        try {
            openFile(o);
        } catch (IOException ex) {
            Exceptions.printStackTrace(ex);
        }
    }

    @Override
    protected void componentClosed() {
        super.componentClosed();
        if (!OutlineView.findInstance().isOpened() && !ThumbView.findInstance().isOpened()) {
            if (old != null) {
                old.requestActive();
            }
        }
    }

    @Override
    protected void componentHidden() {
        doView(OutlineView.findInstance(), false);
        doView(ThumbView.findInstance(), false);
        super.componentHidden();
    }

    @Override
    protected void componentShowing() {
        super.componentShowing();
        doView(OutlineView.findInstance(), outlineButton.isSelected());
        doView(ThumbView.findInstance(), thumbButton.isSelected());
    }

    public void gotoPage(int pagenum) {
        if (pagenum < 0) {
            pagenum = 0;
        } else if (pagenum >= curFile.getNumPages()) {
            pagenum = curFile.getNumPages() - 1;
        }
        forceGotoPage(pagenum);
    }

    /**
     * Changes the displayed page.
     * @param pagenum the page to display
     */
    public void forceGotoPage(int pagenum) {
        if (pagenum <= 0) {
            pagenum = 0;
        } else if (pagenum >= curFile.getNumPages()) {
            pagenum = curFile.getNumPages() - 1;
        }
        curpage = pagenum;

        // update the page text field
        pageField.setText(String.valueOf(curpage + 1));

        // fetch the page and show it in the appropriate place
        PDFPage pg = curFile.getPage(pagenum + 1);
        if (fspp != null) {
            fspp.showPage(pg);
            fspp.requestFocus();
        } else {
            page.showPage(pg);
            page.requestFocus();
        }
        // update the thumb panel
        if (thumbs != null) {
            thumbs.pageShown(pagenum);
        }
        // stop any previous page prepper, and start a new one
        if (pagePrep != null) {
            pagePrep.quit();
        }
        pagePrep = new PagePreparer(pagenum);
        pagePrep.start();

        setEnabling();
    }

    /**
     * A class to pre-cache the next page for better UI response
     */
    class PagePreparer extends Thread {

        int waitforPage;
        int prepPage;

        /**
         * Creates a new PagePreparer to prepare the page after the current
         * one.
         * @param waitforPage the current page number, 0 based
         */
        public PagePreparer(int waitforPage) {
            setDaemon(true);

            this.waitforPage = waitforPage;
            this.prepPage = waitforPage + 1;
        }

        public void quit() {
            waitforPage = -1;
        }

        @Override
        public void run() {
            Dimension size = null;
            if (fspp != null) {
                fspp.waitForCurrentPage();
                size = fspp.getCurSize();
            } else if (page != null) {
                page.waitForCurrentPage();
                size = page.getCurSize();
            }

            if (waitforPage == curpage) {
                PDFPage pdfPage = curFile.getPage(prepPage + 1, true);
                if (pdfPage != null && waitforPage == curpage) {
                    pdfPage.getImage(size.width, size.height, null, null, true, true);
                }
            }
        }
    }

    /**
     * Enable or disable all of the actions based on the current state.
     */
    public void setEnabling() {
        boolean fileavailable = curFile != null;
        boolean pageshown = ((fspp != null) ? fspp.getPage() != null : page.getPage() != null);

        pageField.setEnabled(fileavailable);
        fullScreenAction.setEnabled(pageshown);
        prevAction.setEnabled(pageshown);
        nextAction.setEnabled(pageshown);
        firstAction.setEnabled(fileavailable);
        lastAction.setEnabled(fileavailable);
        fitPageAction.setEnabled(pageshown);
        fitHeightAction.setEnabled(pageshown);
        fitWidthAction.setEnabled(pageshown);
        actualSizeAction.setEnabled(pageshown);
        zoomInAction.setEnabled(pageshown);
        zoomOutAction.setEnabled(pageshown);
    }

    /**
     * Open a specific pdf file.  Creates a DocumentInfo from the file,
     * and opens that.
     */
    public void openFile(PDFDataObject o) throws IOException {
        FileObject fo = o.getPrimaryFile();
        setName(fo.getName());

        RandomAccessFile f = new RandomAccessFile(FileUtil.toFile(fo), "r");
        FileChannel channel = f.getChannel();
        ByteBuffer buf = channel.map(FileChannel.MapMode.READ_ONLY, 0, channel.size());
        this.curFile = new PDFFile(buf);

        if (curFile.isPrintable()) {
            o.addCookie(new PDFPrintSupport(curFile, fo.getName()));
        }

        SwingUtilities.invokeLater(new Runnable() {

            public void run() {
                try {
                    outline = curFile.getOutline();
                } catch (IOException ioe) {
                }
                if (outline != null) {
                    if (outline.getChildCount() > 0) {
                        outlineButton.setEnabled(true);
                    } else {
                        outlineButton.setEnabled(false);
                    }
                } else {
                    outlineButton.setEnabled(false);
                }

                Mode m = WindowManager.getDefault().findMode("explorer");
                old = m.getSelectedTopComponent();
            }
        });
        forceGotoPage(0);
    }

    public void doZoom(double z) {
        if (fspp == null) {
            page.setZoom(z);
        }
    }

    /**
     * Enter full screen mode
     * @param force true if the user should be prompted for a screen to
     * use in a multiple-monitor setup.  If false, the user will only be
     * prompted once.
     */
    public void doFullScreen(boolean force) {
        setFullScreenMode(fullScreen == null, force);
    }

    /**
     * Goes to the next page
     */
    public void doNext() {
        gotoPage(curpage + 1);
    }

    /**
     * Goes to the previous page
     */
    public void doPrev() {
        gotoPage(curpage - 1);
    }

    /**
     * Goes to the first page
     */
    public void doFirst() {
        gotoPage(0);
    }

    /**
     * Goes to the last page
     */
    public void doLast() {
        gotoPage(curFile.getNumPages() - 1);
    }

    /**
     * Goes to the page that was typed in the page number text field
     */
    public void doPageTyped() {
        int pagenum = -1;
        try {
            pagenum = Integer.parseInt(pageField.getText()) - 1;
        } catch (NumberFormatException nfe) {
        }
        if (pagenum >= curFile.getNumPages()) {
            pagenum = curFile.getNumPages() - 1;
        }
        if (pagenum >= 0) {
            if (pagenum != curpage) {
                gotoPage(pagenum);
            }
        } else {
            pageField.setText(String.valueOf(curpage));
        }
    }

    /**
     * Runs the FullScreenMode change in another thread
     */
    class PerformFullScreenMode implements Runnable {

        boolean force;

        public PerformFullScreenMode(boolean forcechoice) {
            force = forcechoice;
        }

        public void run() {
            fspp = new PagePanel();
            fspp.setBackground(Color.black);
            page.showPage(null);
            fullScreen = new FullScreenWindow(fspp, force);
            fspp.addKeyListener(PDFViewer.this);
            gotoPage(curpage);
            fullScreenAction.setEnabled(true);
        }
    }

    /**
     * Starts or ends full screen mode.
     * @param full true to enter full screen mode, false to leave
     * @param force true if the user should be prompted for a screen
     * to use the second time full screen mode is entered.
     */
    public void setFullScreenMode(boolean full, boolean force) {
        //	curpage= -1;
        if (full && fullScreen == null) {
            fullScreenAction.setEnabled(false);
            new Thread(new PerformFullScreenMode(force)).start();
            fullScreenButton.setSelected(true);
        } else if (!full && fullScreen != null) {
            fullScreen.close();
            fspp = null;
            fullScreen = null;
            gotoPage(curpage);
            fullScreenButton.setSelected(false);
        }
    }
    /**
     * Handle a key press for navigation
     */
    int lastPos = 0;

    public void keyPressed(KeyEvent evt) {
        int code = evt.getKeyCode();
        if (code == KeyEvent.VK_LEFT) {
            doPrev();
        } else if (code == KeyEvent.VK_RIGHT) {
            doNext();
        } else if (code == KeyEvent.VK_UP) {
            if (fullScreenButton.isSelected()) {
                doPrev();
            } else {
                unitScrollUp.actionPerformed(new ActionEvent(pageScroll, code, TOOL_TIP_TEXT_KEY));
                if (pageScroll.getVerticalScrollBar().getValue() == lastPos) {
                    doPrev();
                }
            }
        } else if (code == KeyEvent.VK_DOWN) {
            if (fullScreenButton.isSelected()) {
                doNext();
            } else {
                unitScrollDown.actionPerformed(new ActionEvent(pageScroll, code, TOOL_TIP_TEXT_KEY));
                if (pageScroll.getVerticalScrollBar().getValue() == lastPos) {
                    doNext();
                }
            }
        } else if (code == KeyEvent.VK_HOME) {
            doFirst();
        } else if (code == KeyEvent.VK_END) {
            doLast();
        } else if (code == KeyEvent.VK_PAGE_UP) {
            if (fullScreenButton.isSelected()) {
                doPrev();
            } else {
                scrollUp.actionPerformed(new ActionEvent(pageScroll, code, TOOL_TIP_TEXT_KEY));
                if (pageScroll.getVerticalScrollBar().getValue() == lastPos) {
                    doPrev();
                }
            }
        } else if (code == KeyEvent.VK_PAGE_DOWN) {
            if (fullScreenButton.isSelected()) {
                doNext();
            } else {
                scrollDown.actionPerformed(new ActionEvent(pageScroll, code, TOOL_TIP_TEXT_KEY));
                if (pageScroll.getVerticalScrollBar().getValue() == lastPos) {
                    doNext();
                }
            }
        } else if (code == KeyEvent.VK_SPACE) {
            doNext();
        } else if (code == KeyEvent.VK_ESCAPE) {
            setFullScreenMode(false, false);
        }
        lastPos = pageScroll.getVerticalScrollBar().getValue();
    }

    /**
     * Combines numeric key presses to build a multi-digit page number.
     */
    class PageBuilder implements Runnable {

        int value = 0;
        long timeout;
        Thread anim;
        static final long TIMEOUT = 500;

        /** add the digit to the page number and start the timeout thread */
        public synchronized void keyTyped(int keyval) {
            value = value * 10 + keyval;
            timeout = System.currentTimeMillis() + TIMEOUT;
            if (anim == null) {
                anim = new Thread(this);
                anim.start();
            }
        }

        /**
         * waits for the timeout, and if time expires, go to the specified
         * page number
         */
        public void run() {
            long now, then;
            synchronized (this) {
                now = System.currentTimeMillis();
                then = timeout;
            }
            while (now < then) {
                try {
                    Thread.sleep(timeout - now);
                } catch (InterruptedException ie) {
                }
                synchronized (this) {
                    now = System.currentTimeMillis();
                    then = timeout;
                }
            }
            synchronized (this) {
                gotoPage(value - 1);
                anim = null;
                value = 0;
            }
        }
    }
    PageBuilder pb = new PageBuilder();

    public void keyReleased(KeyEvent evt) {
    }

    /**
     * gets key presses and tries to build a page if they're numeric
     */
    public void keyTyped(KeyEvent evt) {
        char key = evt.getKeyChar();
        if (key >= '0' && key <= '9') {
            int val = key - '0';
            pb.keyTyped(val);
        }
    }

    public void valueChanged(TreeSelectionEvent e) {
        if (e.isAddedPath()) {
            OutlineNode node = (OutlineNode) e.getPath().getLastPathComponent();
            if (node == null) {
                return;
            }
            try {
                PDFAction action = node.getAction();
                if (action == null) {
                    return;
                }

                if (action instanceof GoToAction) {
                    PDFDestination dest = ((GoToAction) action).getDestination();
                    if (dest == null) {
                        return;
                    }

                    PDFObject page = dest.getPage();
                    if (page == null) {
                        return;
                    }

                    int pageNum = curFile.getPageNumber(page);
                    if (pageNum >= 0) {
                        gotoPage(pageNum);
                    }
                }
            } catch (IOException ioe) {
                ioe.printStackTrace();
            }
        }
    }
}